#!/bin/sh
#-------------------------------------------------------------------------+
# Copyright (C) 2016 Matt Churchyard (churchers@gmail.com)
# All rights reserved
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# switch libraries
#
. "${LIB}/vm-switch-netgraph"
. "${LIB}/vm-switch-manual"
. "${LIB}/vm-switch-standard"
. "${LIB}/vm-switch-vale"
. "${LIB}/vm-switch-vxlan"

# create switches from rc list on init
# this should run once per boot to make sure switches from the
# configuration file have bridge interfaces. If any new switches are
# created, the create function takes care of setting them up
#
switch::init(){
    local _switchlist _switch _type

    config::core::get "_switchlist" "switch_list"

    if [ -n "${_switchlist}" ]; then
        for _switch in ${_switchlist}; do
            # get the switch type
            config::core::get "_type" "type_${_switch}"

            case "${_type}" in
                vxlan)    switch::vxlan::init "${_switch}" ;;
                manual)   switch::manual::init "${_switch}" ;;
                vale)     ;;
                netgraph) ;;
                *)        switch::standard::init "${_switch}" ;;
            esac
        done
    fi
}

# list switches configured
#
switch::list(){
    local _switchlist _switch _type
    local _id _format="%s^%s^%s^%s^%s^%s^%s^%s\n"

    config::core::get "_switchlist" "switch_list"

    {
        printf "${_format}" "NAME" "TYPE" "IFACE" "ADDRESS" "PRIVATE" "MTU" "VLAN" "PORTS"

        if [ -n "${_switchlist}" ]; then
            for _switch in ${_switchlist}; do
                # get the switch type
                config::core::get "_type" "type_${_switch}"

                case "${_type}" in
                    netgraph) switch::netgraph::show "${_switch}" "${_format}" ;;
                    vale)     switch::vale::show "${_switch}" "${_format}" ;;
                    vxlan)    switch::vxlan::show "${_switch}" "${_format}" ;;
                    manual)   switch::manual::show "${_switch}" "${_format}" ;;
                    *)        switch::standard::show "${_switch}" "${_format}" ;;
                esac
            done
        fi
    } | column -ts^
}

# create a new virtual switch
#
# @param string _switch name of the switch to create
#
switch::create(){
    local _switch
    local _type="standard"
    local _list _curr _vlan _if _bridge _addr _mtu _priv

    # process options
    while getopts t:i:n:b:a:m:p _opt; do
        case ${_opt} in
            t) _type="${OPTARG}" ;;
            i) _if="${OPTARG}" ;;
            n) _vlan="${OPTARG}" ;;
            b) _bridge="${OPTARG}" ;;
            a) _addr="${OPTARG}" ;;
            m) _mtu="${OPTARG}" ;;
            p) _priv="yes" ;;
            *) util::usage ;;
        esac
    done

    shift $((OPTIND - 1))
    _switch="$1"

    # check for a valid switch name
    util::check_name "${_switch}" || util::err "invalid switch name - '${_switch}'"

    # make sure it's not an existing name
    config::core::get "_list" "switch_list"

    for _curr in ${_list}; do
        [ "${_switch}" = "${_curr}" ] && util::err "switch ${_switch} already exists"
    done

    # check vlan number
    if [ -n "${_vlan}" ]; then
        echo "${_vlan}" | egrep -qs '^[0-9]{1,4}$'
        [ $? -eq 0 ] || util::err "invalid vlan number"
        [ ${_vlan} -ge 4095 ] && util::err "invalid vlan number"
    fi

    # check address
    if [ -n "${_addr}" ]; then
        echo "${_addr}" | egrep -qs '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}$'
        [ $? -eq 0 ] || util::err "address must be supplied in CIDR notation (a.b.c.d/prefix-len)"
    fi

    # check mtu
    if [ -n "${_mtu}" ]; then
        echo "${_mtu}" | egrep -qs '^[0-9]{3,4}$'
        [ $? -eq 0 ] || util::err "invalid mtu"
        [ ${_mtu} -gt 9000 ] && util::err "invalid mtu"
    fi

    # check switch type
    case "${_type}" in
        standard) switch::standard::create ;;
        manual)   switch::manual::create ;;
        netgraph) switch::netgraph::create ;;
        vale)     switch::vale::create ;;
        vxlan)    switch::vxlan::create ;;
        *)        util::err "invalid switch type - '${_type}'" ;;
    esac
}

# destroy a switch
# remove from configuration and unload any interfaces we created
#
# @param string _switch name of the switch to remove
#
switch::remove(){
    local _switch="$1"
    local _type

    [ -z "${_switch}" ] && util::usage

    # get the type of switch
    config::core::get "_type" "type_${_switch}"

    case "${_type}" in
        standard) switch::standard::remove "${_switch}" ;;
        manual)   switch::manual::remove "${_switch}" ;;
        netgraph) switch::netgraph::remove "${_switch}" ;;
        vale)     switch::vale::remove "${_switch}" ;;
        vxlan)    switch::vxlan::remove "${_switch}" ;;
        *)        util::err "unable to remove switch of unknown type" ;;
    esac

    # remove all configuration if there's no error
    if [ $? -eq 0 ]; then
        config::core::remove "switch_list" "${_switch}"
        config::core::remove "ports_${_switch} vlan_${_switch} nat_${_switch} type_${_switch}"
        config::core::remove "addr_${_switch} private_${_switch} mtu_${_switch}"
    else
        util::err "failed to remove virtual switch"
    fi

    # make sure the exit status indicates success,
    # even if config::core::remove did not
    return 0
}

# add a new interface to a switch
#
# @param string _switch name of the switch
# @param string _if the interface to add
#
switch::add_member(){
    local _switch="$1"
    local _if="$2"
    local _type

    [ -z "${_switch}" -o -z "${_if}" ] && util::usage

    # get the type of switch
    config::core::get "_type" "type_${_switch}"

    case "${_type}" in
        standard) switch::standard::add_member "${_switch}" "${_if}" ;;
        manual)   switch::manual::add_member "${_switch}" "${_if}" ;;
        netgraph) switch::netgraph::add_member "${_switch}" "${_if}" ;;
        vale)     switch::vale::add_member "${_switch}" "${_if}" ;;
        vxlan)    switch::vxlan::add_member "${_switch}" "${_if}" ;;
        *)        util::err "unable to configure switch of unknown type" ;;
    esac
}

# remove a member interface from a virtual switch
#
# @param string _switch name of the switch
# @param string _if the interface to remove
#
switch::remove_member(){
    local _switch="$1"
    local _if="$2"
    local _type

    [ -z "${_switch}" -o -z "${_if}" ] && util::usage

    # get the type of switch
    config::core::get "_type" "type_${_switch}"

    case "${_type}" in
        standard) switch::standard::remove_member "${_switch}" "${_if}" ;;
        manual)   switch::manual::remove_member "${_switch}" "${_if}" ;;
        netgraph) switch::netgraph::remove_member "${_switch}" "${_if}" ;;
        vale)     switch::vale::remove_member "${_switch}" "${_if}" ;;
        vxlan)    switch::vxlan::remove_member "${_switch}" "${_if}" ;;
        *)        util::err "unable to configure switch of unknown type" ;;
    esac
}

# change the vlan number on a virtual switch
#
# @param string _switch name of the switch
# @param int _vlan the vlan number (0 to turn vlan off)
#
switch::vlan(){
    local _switch="$1"
    local _vlan="$2"
    local _id _type

    [ -z "${_switch}" -o -z "${_vlan}" ] && util::usage

    switch::id "_id" "${_switch}"
    switch::type "_type" "${_switch}"
    [ -z "${_id}" ] && util::err "unable to locate specified virtual switch"

    echo "${_vlan}" | egrep -qs '^[0-9]{1,4}$'
    [ $? -eq 0 ] || util::err "invalid vlan number"
    [ ${_vlan} -ge 4095 ] && util::err "invalid vlan number"

    case "${_type}" in
        standard) switch::standard::vlan "${_switch}" "${_vlan}" ;;
        manual)   switch::manual::vlan "${_switch}" "${_vlan}" ;;
        netgraph) switch::netgraph::vlan "${_switch}" "${_vlan}" ;;
        vale)     switch::vale::vlan "${_switch}" "${_vlan}" ;;
        vxlan)    switch::vxlan::vlan "${_switch}" "${_vlan}" ;;
        *)        util::err "unable to configure switch of unknown type" ;;
    esac
}

# enable or diable private flag on a switch
# note that we don't update existing interfaces; this 
# makes things easy for us and any guests booted after
# will get the new setting
#
# @param string _switch the switch to update
# @param string _priv on,yes|off,no
#
switch::private(){
    local _switch="$1"
    local _priv="$2"
    local _type

    # try to get switch type
    [ -z "${_switch}" -o -z "${_priv}" ] && util::usage
    switch::type "_type" "${_switch}" || util::err "specified switch does not appear to be valid"

    case "${_type}" in
        standard|manual|vxlan)
            if util::yesno "${_priv}"; then
                config::core::set "private_${_switch}" "yes"
            else
                config::core::set "private_${_switch}" "no"
            fi
            ;;
        netgraph)
            util::err "unable to configure private mode on netgraph switches"
            ;;
        vale)
            util::err "unable to configure private mode on vale switches"
            ;;
        *)
            util::err "unable to configure switch of unknown type"
            ;;
    esac
}

# enable or disable nat functionality on a virtual switch
#
# @param string _switch name of the switch
# @param string _nat on|off
#
switch::nat(){
    util::warn "internal nat support is currently disabled"
    util::warn "please add an address to the virtual switch and configure your firewall for NAT manually"
}

# set or remove ip address from a virtual switch
#
# @param string _switch name of the switch
# @param string _addr the ip address to add (or "none" to remove
#
switch::address(){
    local _switch="$1"
    local _addr="$2"
    local _id _type

    [ -z "${_switch}" ] && util::usage

    switch::id "_id" "${_switch}"
    switch::type "_type" "${_switch}"
    [ -z "${_id}" ] && util::err "unable to locate specified virtual switch"

    # check address
    if [ -n "${_addr}" ] && [ "${_addr}" != "none" ]; then
        echo "${_addr}" | egrep -qs '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}$'
        [ $? -eq 0 ] || util::err "address must be supplied in CIDR notation (a.b.c.d/prefix-len)"
    fi

    case "${_type}" in
        standard) switch::standard::address "${_switch}" "${_addr}" ;;
        manual)   ;&
        netgraph) ;&
        vale)     ;&
        vxlan)    util::err "feature not currently supported on switches of this type" ;;
        *)        util::err "unable to configure switch of unknown type" ;;
    esac
}

# return the type for a switch
#
# @param string _var variable to put type into
# @param string _switch the switch name
#
switch::type(){
    local _var="$1"
    local _switch="$2"

    [ -z "${_switch}" ] && return 1
    config::core::get "${_var}" "type_${_switch}" "standard"
}

# check if a switch is configured for private members
#
# @param string _switch switch name
# @return succes (0) if it's private
#
switch::is_private(){
    local _switch="$1"
    local _priv

    config::core::get "_priv" "private_${_switch}"
    util::yesno "${_priv}"
}

# get the bridge id for a virtual switch
#
# @param string _var variable to put name into
# @param string _switch the name of the switch
#
switch::id(){
    local _var="$1"
    local _switch="$2"
    local _type

    [ -z "${_switch}" ] && return 1

    # get switch type
    config::core::get "_type" "type_${_switch}"

    case "${_type}" in
        vale)     switch::vale::id "${_var}" "${_switch}" ;;
        netgraph) switch::netgraph::id "${_var}" "${_switch}" ;;
        manual)   switch::manual::id "${_var}" "${_switch}" ;;
        *)        switch::standard::id "${_var}" "${_switch}" ;;
    esac
}

# get a virt interface id for a port/switch
#
# @param string _var variable name to put result into
# @param string _switch switch name to get id for
#
switch::__viid(){
    local _hash=$(md5 -qs "${2}" | cut -c1-5)
    setvar "$1" "viid-${_hash}@"
}

# retrieve interface name, given a switch name
# we convert to viid then look for the matching group
#
# @param string _var variable to put interface name into
# @param string _switch the switch name
#
switch::find(){
    local _var="$1"
    local _switch="$2"
    local _viid _name

    switch::__viid "_viid" "${_switch}"
    _name=$(ifconfig -g "${_viid}" 2>/dev/null)
    [ -n "${_name}" ] && setvar "${_var}" "${_name}"
}

# mark an interface with a unique viid
# i say unique, its 5 chars from an md5 hash which
# should be enough for half a dozen switches
#
# @parem string _switch switch name
# @param string _iface interface to mark
#
switch::set_viid(){
    local _switch="$1"
    local _iface="$2"
    local _viid

    switch::__viid "_viid" "${_switch}"
    ifconfig "${_iface}" group "${_viid}" >/dev/null 2>&1
}

# create a network interface for a guest
# relies heavily on variables set in the main vm::run function
#
# @modifies _func _devices
#
switch::provision(){
    local _switch _mac _type

    config::get "_switch" "network${_num}_switch"
    config::get "_mac" "network${_num}_mac"

    # set a static mac if we don't have one
    [ -z "${_mac}" ] && vm::generate_static_mac

    switch::type "_type" "${_switch}"

    case "${_type}" in
        vale)     switch::vale::provision ;;
        netgraph) switch::netgraph::provision ;;
        standard) ;&
        manual)   ;&
        vxlan)    switch::standard::provision ;;
        *)        util::warn "unable to configure interface ${_num}" ;;
    esac
}
